既然有了新網頁,當然要好好的測試一下囉!
今天我們來聊聊怎麼對新網頁進行測試!
這個網頁比起前面的 hello world 要稍微複雜一些,所以我們的測試也會稍微複雜。
首先,在整合測試的部分,我們希望整體的功能最後呈現的是:
inspire
,HTTP status 是 200各個元件的部分,因為之前的功能並沒有細分成不同元件實作,所以沒有討論。
我們針對 InspireController
,應該要測試:
inspire
函式時,應該要呼叫對應的 InspireService->inspire()
,並回傳其輸出我們針對 InspireService
,應該要測試:
inspire
函式時,應該要能回傳一句名言大概這樣。
整合測試前面已經做過很多次了,相信大家都很熟悉。我們這次測試檔案命名為 tests/Feature/InspireTest.php
(記得怎麼產生這個檔案嗎?忘記的話請回去看看猝不及防的自動測試教學!怎麼用 Laravel 撰寫自動測試)
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
class InspireTest extends TestCase
{
/**
* A basic feature test example.
*
* @return void
*/
public function testInspire()
{
$response = $this->get('/inspire');
$response->assertStatus(200);
}
}
通過之後,我們來規劃一下單元測試。
現在我們要分別針對 InspireController
和 InspireService
進行測試了。不過這個時候,我們遇到了一個問題⋯⋯
InspireController->inspire()
的內容是
public function inspire()
{
return (new InspiringService())->inspire();
}
直接這樣測試的話,無論如何都會用到 InspiringService
呀!這該怎麼辦?
遇到這個問題,根本原因是因為,我們的 InspireController
,其服務依賴於底層架構,也就是 InspiringService
的實作。
這樣的設計很常見,也很符合開發的思路。
不過,這樣的設計,相對來說缺乏彈性。舉現在的例子,當你想要測試時,就被這層依賴關係限制住,無法抽換了。
所以,我們要反轉這層依賴關係,使用依賴注入的方式進行實作。
首先,我們在 InspireController
裡面,加入一個 constructor:
public function __construct()
{
}
這是 PHP 裡面宣告建構子(constructor)的方式,每次建立 InspireController
時,都會先呼叫這個函式,來完成 InspireController
的初始化。
接著,我們這樣改寫:
<?php
namespace App\Http\Controllers;
use App\Services\InspiringService;
use Illuminate\Http\Request;
class InspiringController extends Controller
{
private $service;
public function __construct(InspiringService $inspiringService)
{
$this->service = $inspiringService;
}
/**
* @return string
*/
public function inspire()
{
return $this->service->inspire();
}
}
這樣改寫有什麼好處?
用這樣的寫法,InspiringController
可以不使用 inspiringService
來完成其任務,而是任何繼承 inspiringService
的物件都可以!換句話說,如果我們想要修改其邏輯,我們只要撰寫某個類別,繼承 inspiringService
就好了。
這樣的寫法,增加了很大的彈性,在我們這次的狀況中,這份彈性正好可以用來完成單元測試的需求。
我們來建立 InspiringControllerTest
這個單元測試:
$ php artisan make:test --unit InspiringControllerTest
成功之後,我們就會在 tests/Unit
裡面看到 InspiringControllerTest.php
然後就是比較複雜的部分了,我們利用 Laravel 內建的 Mockery 來協助我們製作仿造物件(mock):
$mock = \Mockery::mock(InspiringService::class);
這樣撰寫,$mock
這個物件就成功的作為 InspiringService
的 mock。
接著,我們希望這個 mock 不僅僅只是仿造而已,他還能協助我們檢查InspiringController
是否成功呼叫了他的 inspire()
並回傳其內容。
所以,我們這樣做:
$mock->shouldReceive('inspire')->andReturn('名言');
很直觀吧!這樣宣告之後,$mock
會預期被呼叫 inspire
,然後回傳「名言」
這樣設計環境之後,針對 InspiringController
的測試就很好撰寫了。我們可以這樣做:
$inspiringController = new InspiringController($mock);
宣告 InspiringController
時,不是使用 InspiringService
,而是繼承 InspiringService
的 $mock
物件
然後我們斷言 InspiringController->inspire()
的回傳,應該會等同 $mock
的回傳:
self::assertEquals(
'名言',
$inspiringController->inspire()
);
綜合起來就是
<?php
namespace Tests\Unit;
use App\Http\Controllers\InspiringController;
use App\Services\InspiringService;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
class InspiringControllerTest extends TestCase
{
/**
* A basic unit test example.
*
* @return void
*/
public function testInspire()
{
$mock = \Mockery::mock(InspiringService::class);
$mock->shouldReceive('inspire')->andReturn('名言');
$inspiringController = new InspiringController($mock);
self::assertEquals(
'名言',
$inspiringController->inspire()
);
}
}
這樣,我們的測試就完成了。
相比之下,InspiringService
就好測試多了,目前我們只需要確定他的 inspire()
確實會回傳字串而已:
動作如下:
tests/Unit/InspiringServiceTest.php
testInspire()
,呼叫 InspiringService->inspire()
,並確定回傳的型態是字串內容如下:
<?php
namespace Tests\Unit;
use App\Services\InspiringService;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
class InspiringServiceTest extends TestCase
{
/**
* A basic unit test example.
*
* @return void
*/
public function testExample()
{
self::assertIsString(
(new InspiringService())->inspire()
);
}
}
很簡單吧!
今天的文章結束囉!總結一下我們學到了什麼。
今天我們知道了 Laravel 裡面怎麼新增單元測試,知道怎麼用依賴注入的方式反轉原先的依賴關係。還學會了怎麼使用 Mockery!
今天的內容有點多,希望大家吸收起來還順利,我們明天見!
Inspire 與 Inspiring 錯亂了
寫到一半時我後來全部都改成 inspire
比較簡單
途中改來改去真的很不好意思 ><
照著上面提供的方法做會出現紅色毛毛蟲
這要如何修正呢
這個是因為編輯器內建的語法矯正器發現你提供的 class 與 controller 需要的參數並不一致造成的
$this->mock(InspireService::class);
$inspireController = $this->app->make(InspireController::class);